package dllimports

import (
	"errors"
	"runtime"
	"sync"
	"sync/atomic"
)

type Handle uintptr

// DLL library
type DLL struct {
	name   string
	handle Handle
}

func NewDLL(name string) (*DLL, error) {
	switch runtime.GOOS {
	case "windows", "linux", "darwin":
		return newDLL(name)
	default:
		return nil, errors.New("un support os")
	}
}

func (d *DLL) Release() error {
	if d.handle == 0 {
		return nil
	}

	return d.release(d.handle)
}

func (d *DLL) FindProc(name string) (*ProcAddr, error) {
	if d.handle == 0 {
		return nil, errors.New("dll handle is nil")
	}
	return d.findProc(d.handle, name)
}

// ProcAddr 函数/过程指针
type ProcAddr struct {
	dll  *DLL
	name string
	addr Handle
}

// Call func
//
// int: [IN] uintptr(int)
// int: [OUT]
//
//	var age int;
//	uintptr(unsafe.Pointer(&age))
//
// string: [IN] dllimports.StringToUTF8("")
// string: [OUT]
//
//	buff := make([]uint8, 255)
//	r1, _, err := xxx.Call(addr, uintptr(unsafe.Pointer(&buff[0])), uintptr(len(buff)))
//
// struct: [IN, OUT]
//
//	RECT rect;
//	uintptr(unsafe.Pointer(&rect))
//
// return:
//
//	r1, _, err := xxx.Call(addr, ...)
//	if r1 == 0 {
//	  return nil, err
//	}
func (p *ProcAddr) Call(args ...uintptr) (r1, r2 uintptr, err error) {
	return p.call(p.addr, args...)
}

func (p *ProcAddr) Addr() uintptr {
	return uintptr(p.addr)
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

type LazyDLL struct {
	mu   sync.Mutex
	dll  atomic.Pointer[DLL]
	Name string
}

func NewLazyDLL(name string) *LazyDLL {
	return &LazyDLL{Name: name}
}

func (l *LazyDLL) LoadDLL() error {

	if l.dll.Load() == nil {
		l.mu.Lock()
		defer l.mu.Unlock()
		if l.dll.Load() == nil {
			dll, err := NewDLL(l.Name)
			if err != nil {
				return err
			}

			l.dll.Store(dll)
		}
	}

	return nil
}

func (l *LazyDLL) NewProc(name string) *LazyProc {
	return &LazyProc{dll: l, mame: name}
}

func (l *LazyDLL) findProc(name string) (*ProcAddr, error) {
	if e := l.LoadDLL(); e != nil {
		return nil, e
	}

	return l.dll.Load().FindProc(name)
}

type LazyProc struct {
	mu   sync.Mutex
	mame string
	dll  *LazyDLL

	proc atomic.Pointer[ProcAddr]
}

func (p *LazyProc) Find() error {
	if p.proc.Load() == nil {
		p.mu.Lock()
		defer p.mu.Unlock()

		if p.proc.Load() == nil {
			proc, e := p.dll.findProc(p.mame)
			if e != nil {
				return e
			}

			p.proc.Store(proc)
		}
	}
	return nil
}

func (p *LazyProc) Addr() uintptr {
	if err := p.Find(); err != nil {
		panic(err)
	}
	return p.proc.Load().Addr()
}

func (p *LazyProc) Call(args ...uintptr) (r1, r2 uintptr, lastErr error) {
	if err := p.Find(); err != nil {
		return 0, 0, err
	}

	return p.proc.Load().Call(args...)
}
